﻿
namespace Patterns4Net.PatternEnforcer.Core.Patterns
{
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Mono.Cecil;
    using Mono.Cecil.Cil;
    using Mono.Cecil.Rocks;
    using Patterns4Net.Core.CecilExtensions;
    using Patterns4Net.Core.Patterns;
    using Patterns4Net.Core;
    using MethodCheckers;

    /// <summary>
    /// Represents a checker for the <see cref="Immutable"/> pattern.
    /// </summary>
    [PatternCheckerExport(typeof(Immutable))]
    public sealed class ImmutableChecker : PatternCheckerBase<Immutable>
    {
        private readonly IEnumerable<TypeReference> ignoreDeepImmutability;

        /// <summary>
        /// Initializes a new instance of the <see cref="ImmutableChecker"/> class. 
        /// This default public constructor is explicitly declared, because this class has 
        /// also private constructor <see cref="ImmutableChecker()"/>.
        /// </summary>
        public ImmutableChecker()
        {
            this.ignoreDeepImmutability = Enumerable.Empty<TypeReference>();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ImmutableChecker"/> class, 
        /// when <see cref="ImmutableChecker"/> is recursively called to check deep immutability, 
        /// we have to know which types have been checked, so we does not end up in an infinite loop.
        /// </summary>
        /// <param name="ignoreDeepImmutability">The types that have been already checked for immutability.</param>
        public ImmutableChecker(IEnumerable<TypeReference> ignoreDeepImmutability)
        {
            this.ignoreDeepImmutability = ignoreDeepImmutability;
        }

        protected override CheckerResult Check(Immutable pattern)
        {
            var typeDefinition = pattern.TargetType;
            var result = new CheckerResultBuilder();

            // All public fields should be readonly - so they cannot be changed from outside
            PublicFieldsAreReadonly(typeDefinition, result);

            // Get all accessible instance fields
            // Internal state of those who are of our type should not be changed in any method...
            // Internal state of those who are not of other type should not be changed if deep = true...
            var accessibleFields = new List<FieldReference>(typeDefinition.Fields.Where(x => IsAccessible(typeDefinition, x)));
            foreach (var method in typeDefinition.GetMethods())
            {
                if (IsAccessible(typeDefinition, method))
                {
                    AddAccessibleFields(method, accessibleFields);
                }
            }

            // Find all methods that are accessible from "outside"
            var accessibleMethods = new List<MethodAndParameters>();
            var fieldsOfMyType = accessibleFields.Where(x => x.FieldType.IsEqual(typeDefinition));
            foreach (var method in typeDefinition.GetMethods())
            {
                if (IsAccessible(typeDefinition, method))
                {
                    AddMethodsUsedIn(method, accessibleMethods, fieldsOfMyType);
                }                
            }

            // Check, that methods accessible from outside dont change internal state of this instance,
            // or internal state of any field that is of the same type as me...
            foreach (var accessibleMethod in accessibleMethods)
            {
                var methodChecker = new DoesNotChangeInternalState(accessibleMethod.ParametersFromOutside, fieldsOfMyType);
                result.AddChildResult(methodChecker.Check(accessibleMethod.Method));
            }
            
            if (pattern.Deep == false)
            {
                // if we dont have to check deep immutability, we can end now...
                return result.Build();
            }

            // Deep immutability:
            var typesToBeChecked = accessibleFields.Except(fieldsOfMyType)
                .Where(field => this.ignoreDeepImmutability.None(type => field.FieldType.IsEqual(type)))
                .Select(field => field.FieldType)
                .Distinct((a, b) => a.IsEqual(b));
            foreach (var type in typesToBeChecked)
            {
                if (type.IsPrimitive || type.IsEqual(typeof(string)))
                {
                    // a little bit of optimalization - these types are immutable for sure.
                    continue;
                }

                var resolved = type.Resolve();
                if (resolved == null)
                {
                    // we cannot check types we cannot resolve, therefore we will just continue
                    continue;
                }

                var checker = new ImmutableChecker(this.ignoreDeepImmutability.Concat(pattern.TargetType));                
                var nestedResult = checker.Check(new Immutable(resolved, true));
                result.AddChildResult(nestedResult);
            }

            return result.Build();
        }

        private static void AddAccessibleFields(MethodDefinition method, IList<FieldReference> fields)
        {
            if (method.IsVoid() || method.HasBody == false)
            {
                return;
            }

            var stackStates = StackObserver.CreateStackStates(method);
            method.Body.SimplifyMacros();
            for (int i = 0; i < method.Body.Instructions.Count; i++)
            {
                var currentInstruction = method.Body.Instructions[i];
                if (currentInstruction.OpCode == OpCodes.Ret)
                {
                    var returnValue = stackStates[i - 1].Last();
                    var roots = StackObserver.GetPossibleRoots(method, stackStates, returnValue);
                    foreach (var stackItem in roots.Where(x => x.IsField && x.FieldOwner != null && x.FieldOwner.IsThisParameter))
                    {
                        fields.Add(stackItem.FieldReference);
                    }

                    // select all calls of instance method whose return value might be a source for a 
                    // return value of our current method...
                    var calls = from stackItem in roots
                                where stackItem.IsMethodReturnValue
                                let calledMethod = stackItem.PushInstruction.Operand as MethodDefinition
                                where calledMethod != null
                                where calledMethod.IsStatic() == false
                                where calledMethod.DeclaringType.IsEqual(method.DeclaringType)
                                where calledMethod.IsEqual(method) == false
                                select stackItem;
                    foreach (var stackItem in calls)
                    {
                        AddAccessibleFields(stackItem.PushInstruction.Operand as MethodDefinition, fields);
                    }
                }
            }
        }

        private static bool IsAccessible(TypeDefinition type, MethodDefinition method)
        {
            return method.IsPublic || method.IsAssembly || (type.IsSealed == false && method.IsFamily);
        }

        private static bool IsAccessible(TypeDefinition type, FieldDefinition field)
        {
            return field.IsPublic || field.IsAssembly || (type.IsSealed == false && field.IsFamily);
        }

        private static void PublicFieldsAreReadonly(TypeDefinition typeDefinition, CheckerResultBuilder result)
        {
            foreach (var field in typeDefinition.Fields)
            {
                if (field.IsStatic)
                {
                    continue;
                }

                result.AddCheckedRule();
                if (field.IsPublic && field.IsInitOnly == false)
                {
                    result.Add(ErrorMessages.ImmutablePublicFieldMustBeReadOnly, field.Name, typeDefinition.Name);
                }
            }
        }

        private static void AddMethodsUsedIn(
            MethodDefinition methodDefinition, 
            IList<MethodAndParameters> used, 
            IEnumerable<FieldReference> evilFields)
        {
            // We will walk thru call graph recursively using this stack:
            var methodsStack = new Stack<MethodAndParameters>();

            // The starting method is public method, so all parameters are evil 
            // - may be cause of change of internal state.
            var evilParams = methodDefinition.Parameters
                .Where(p => p.ParameterType.IsEqual(methodDefinition.DeclaringType));
            methodsStack.Push(new MethodAndParameters(methodDefinition, evilParams));

            while (methodsStack.Count > 0)
            {
                var currentMethod = methodsStack.Pop();
                used.Add(currentMethod);
                AddUsedMethodsToStack(currentMethod, methodsStack, evilFields);
            }
        }

        private static void AddUsedMethodsToStack(
            MethodAndParameters methodCallInfo, 
            Stack<MethodAndParameters> methodsStack, 
            IEnumerable<FieldReference> evilFields)
        {
            if (methodCallInfo.Method.HasBody == false)
            {
                return;
            }

            MethodDefinition currentMethod = methodCallInfo.Method;
            var stackStates = StackObserver.CreateStackStates(currentMethod);
            for (int i = 0; i < currentMethod.Body.Instructions.Count; i++)
            {
                var instruction = currentMethod.Body.Instructions[i];                
                if (instruction.IsCallInstruction())
                {
                    var calledMethodReference = instruction.Operand as MethodReference;
                    Contract.Assert(calledMethodReference != null);

                    // If we are not able to resolve the method, it won't do anything wrong with out type.
                    var calledMethod = calledMethodReference.Resolve();
                    if (calledMethod == null)
                    {
                        continue;
                    }

                    // Is it a method from the type that we are checking?
                    if (calledMethod.DeclaringType == currentMethod.DeclaringType)
                    {
                        if (calledMethod.IsStatic)
                        {
                            var evilParams = GetEvilMethodParameters(methodCallInfo, calledMethod, stackStates, evilFields, i);
                            if (evilParams.Any())
                            {
                                methodsStack.Push(new MethodAndParameters(calledMethod, evilParams));
                            }
                        }
                        else
                        {
                            // Now the method we have is called on some instance.
                            // We will examine what it is.                           
                            var args = stackStates[i - 1].GetArguments(calledMethod);
                            var possibleSourcesOfFirstParam = StackObserver.GetPossibleRoots(currentMethod, stackStates, args.First());

                            // If the instance is 'this' - we might change the internal state in this method,
                            // if the instance is parameter that came from 'outside', 
                            // we're changing the internal state as well.
                            if (possibleSourcesOfFirstParam.Any(x => x.IsThisParameter) ||
                                possibleSourcesOfFirstParam.Any(x => IsEvilParameter(methodCallInfo, x)))
                            {
                                var evilParams = GetEvilMethodParameters(methodCallInfo, calledMethod, stackStates, evilFields, i);
                                methodsStack.Push(new MethodAndParameters(calledMethod, evilParams));
                            }
                        }
                    }
                }
            }
        }

        private static bool IsEvilParameter(MethodAndParameters currentMethod, StackItem parameterStackItem)
        {
            return parameterStackItem.IsParameter &&
                currentMethod.ParametersFromOutsideNumbers.Contains(parameterStackItem.ParameterIndex);
        }

        /// <summary>
        /// Returns indexes of those parameters that have a value that came from "outside".
        /// </summary>
        private static IEnumerable<ParameterDefinition> GetEvilMethodParameters(
            MethodAndParameters methodCallInfo, 
            MethodDefinition calledMethod, 
            StackState[] stackStates,
            IEnumerable<FieldReference> evilFields,
            int methodCallInstructionIndex)
        {
            var args = stackStates[methodCallInstructionIndex - 1].GetArguments(calledMethod);
            bool isInstanceMethod = calledMethod.IsStatic == false;
            if (isInstanceMethod)
            {
                args = args.Skip(1).ToArray(); // skip 'this' argument
            }
            
            // Now we will examine parameters that are passed to this method
            var result = new List<ParameterDefinition>();
            for (int argNum = 0; argNum < calledMethod.Parameters.Count; argNum++)
            {
                // stackItem is an instruction which pushed parameter with number argNum onto the stack.
                var stackItem = args[argNum];
                var possibleSources = StackObserver.GetPossibleRoots(methodCallInfo.Method, stackStates, stackItem);

                // Is it an argument that comes from "outside"?
                var evilSource = possibleSources.FirstOrDefault(x => x.IsParameter &&
                    methodCallInfo.ParametersFromOutsideNumbers.Contains(x.ParameterIndex));
                if (evilSource != null)
                {
                    result.Add(calledMethod.Parameters[argNum]);
                }
                else if (possibleSources.Any(x => x.IsThisParameter)) 
                {
                    result.Add(calledMethod.Parameters[argNum]);
                }
                // is it an evil field? e.i., is it field of our type and is it contained in evilFields list?
                else if (possibleSources.Any(x => x.IsField && 
                    x.FieldReference.DeclaringType.IsEqual(methodCallInfo.Method.DeclaringType) &&
                    evilFields.Any(evil => string.CompareOrdinal(x.FieldReference.FullName, evil.FullName) == 0)))
                {
                    result.Add(calledMethod.Parameters[argNum]);
                }
            }

            return result;
        }

        private class MethodAndParameters
        {
            public MethodAndParameters(MethodDefinition method, IEnumerable<ParameterDefinition> parametersFromOutside)
            {
                this.Method = method;
                this.ParametersFromOutside = parametersFromOutside;
            }

            /// <summary>
            /// Gets parameters whose values come from 'outside' world.             
            /// </summary>
            /// <remarks>
            /// <para>
            /// For example, if a private method with parameters A and B is called in another public method, which 
            /// supplies a new object as value of A, and it's own parameter as value for B, then 
            /// A is not parameter from outside and B is from outside, because it's value was supplied 
            /// by a user who called the public method.
            /// </para>
            /// </remarks>
            public IEnumerable<ParameterDefinition> ParametersFromOutside { get; private set; }

            public MethodDefinition Method { get; private set; }

            public IEnumerable<int> ParametersFromOutsideNumbers
            {
                get { return this.ParametersFromOutside.Select(p => p.Index); }
            }
        }
    }
}
